package Zim::Repository::DBI;

use strict;
use DBI;
use Zim::Repository::Base;

our $VERSION = '0.01';
our @ISA = qw/Zim::Repository::Base/;

=head1 NAME

Zim::Repository::DBI - Database backend for Zim

=head1 DESCRIPTION

Proof-of-concept implementation of a repository using a DBI backend.

!! TODO update api to newest revision TODO !!

=head1 METHODS

=over 4

=cut

our %SQL = (
	list_pages => 'SELECT name FROM `default`',
	get_page  => 'SELECT * FROM `default` WHERE name=?', # name
	page_exists => 'SELECT id FROM `default` WHERE name=?', # name
	update_page => 'UPDATE `default` SET content=? WHERE id =?', # content, id
	insert_page => 'INSERT INTO `default` (name, parent, content) VALUES (?, ?, ?)', # name, parent, context
	move_page => 'UPDATE `default` SET name=? WHERE id=?', # name, id
	delete_page => 'DELETE FROM `default` WHERE id=?', # id
);

=item C<new(NAMESPACE, DSN, USER, PASS)>

Simple constructor.

=cut

sub init { # called by new
	my ($self, $dsn, $user, $pass) = @_;
	warn "R:DBI->init @_\n";
	$self->{dbh} = DBI->connect($dsn, $user, $pass, {RaiseError => 1});
}

=item C<list_pages(NAMESPACE)>

=cut

sub list_pages {
	my ($self, $namespace) = @_;
	# TODO No namespace support
	
	my $dbh = $self->{dbh};
	my $sth = $dbh->prepare($SQL{list_pages});
	$sth->execute;
	my @pages = map { @$_ } @{ $sth->fetchall_arrayref };
	
	return @pages;
}

=item C<page_exists(NAME)>

=cut

sub page_exists {
	my ($self, $page) = @_;
	
	$page =~ s/(^:*$self->{namespace}:*)//g;
	$page =~ s/:/_/g; # TODO no namespaces
	my ($id) = $self->{dbh}->selectrow_array($SQL{page_exists}, undef, $page);
	warn "got id $id for $page\n";
	return $id; # FALSE when failed
}

=item C<open_page(NAME)>

=cut

sub open_page {
	my ($self, $name) = @_;
	
	my $id = $self->page_exists($name);
	my $source = $name;
	$source =~ s/(^:*$self->{namespace}:*)//g;
	$source =~ s/:/_/g; # TODO no namespaces
	$name = $self->{namespace}.$source; # TODO no namespaces
	print "opening page $name ($source)\n";
	my $page = Zim::Page::Text->new($self, $name);
	$page->status('new') unless $id;
	$page->set_source($source); # source is page name minus our namespace
	$page->set_format('wiki');
	# how to hook template ?

	return $page;
}

=item C<get_source(SOURCE, MODE)>

Returns an IO object derived from IO::Scalar.

=cut

sub get_source {
	my ($self, $source, $mode) = @_;

	if    ($mode eq 'r') { $self->_read_source($source)  }
	elsif ($mode eq 'w') { $self->_write_source($source) }
	else                 { die "Unkown mode: $mode"      }
}

sub _read_source {
	my ($self, $source) = @_;
	
	my $page = $self->{dbh}->selectrow_hashref($SQL{get_page}, undef, $source);
	return unless $page;

	warn "open id $page->{id}\n";
	my $content = $page->{content};
	return IO::Scalar->new(\$content);
}

sub _write_source {
	my ($self, $source) = @_;
	
	my $content;
	return IO::Scalar::OnClose->new(\$content, sub {
			my $ref = shift;
			$self->_on_close_source($source, $$ref);
		} );
}

sub _on_close_source {
	my ($self, $source, $content) = @_;
	
	my ($id) = $self->{dbh}->selectrow_array($SQL{page_exists}, undef, $source);
	if ($id) {
		warn "update id $id\n";
		$self->{dbh}->do($SQL{update_page}, undef, $content, $id);
	}
	else {
		warn "insert new page\n";
		my $page = $source;
		$self->{dbh}->do($SQL{insert_page}, undef, $page, 0, $content);
	}
}

=item C<move_page(SOURCE, TARGET)>

=cut

sub move_page {
	my ($self, $old, $new) = @_;
	warn "move $old to $new\n";

	my $id = $self->page_exists(ref($old) ? $old->name : $old);
	die "No such page: $old\n" unless $id;

	my $source = $new;
	$source =~ s/(^:*$self->{namespace}:*)//g;
	$self->{dbh}->do($SQL{move_page}, undef, $source, $id);

	# Update Page object
	if (ref $old) {
		$old->name($new);
		$old->set_source($new);
	}
}

=item C<delete_page(PAGE)>

=cut

sub delete_page {
	my ($self, $page) = @_;

	my $id = $self->page_exists(ref($page) ? $page->name : $page);
	die "No such page: $page\n" unless $id;
	$page =~ s/(^:*$self->{namespace}:*)//g;
	$self->{dbh}->do($SQL{delete_page}, undef, $id);

	if (ref $page) {
		$page->{parse_tree} = undef;
		$page->status('deleted');
	}
}

package IO::Scalar::OnClose;

use IO::Scalar;
our @ISA = qw/IO::Scalar/;

# Simple class derived from IO::Scalar with the purpose of
# calling back when the handle is closed so we can safe the content.

sub new {
	my ($class, $ref, $hook) = @_;
	my $self = IO::Scalar::new($class, $ref);
	*$self->{hook} = $hook;
	return $self;
}

sub close {
	my $self = shift;
	*$self->{hook}->( *$self->{SR} );
	IO::Scalar::close($self);
}

1;

__END__

=back

=head1 AUTHOR

Jaap Karssenberg (Pardus) E<lt>pardus@cpan.orgE<gt>

Copyright (c) 2006 Jaap G Karssenberg. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=head1 SEE ALSO

=cut

